import mailbox, os
from datetime import datetime
from glob import glob

class DovecotMaildir(mailbox.Maildir):
    """A dovect-style Maildir mailbox.
        See: http://wiki.dovecot.org/MailboxFormat/Maildir
    """
    def __init__(self, dirname, factory=mailbox.MaildirMessage, create=True):
        """Initialize a Dovecot Maildir instance."""
        mailbox.Maildir.__init__(self, dirname, factory, create)
        fpath = os.path.join(self._path, 'dovecot-uidlist')
        if os.path.exists(fpath):
            self._uidlist = fpath
        else:
            if create:
                self._fwrite(self._uidlist, self._get_header(), 'header')
            else:
                raise NoSuchMailboxError(self._path)

    def add(self, message):
        """Punt to mailbox' Maildir implementation to actually write 
            the message to disk, then update the uidlist file accordingly.
        """
        fname = mailbox.Maildir.add(self, message)
        #Update uidlist to include new info
        #TODO: Cache updates and write when flushed?
        self._fwrite(self._uidlist, fname)

    def list_folders(self):
        """Return a list of folder names."""
        result = []
        for entry in os.listdir(self._path):
            #Dovecot uses Maildir++ directory structure, with . as the seperator
            #see: http://www.inter7.com/courierimap/README.maildirquota.html
            if len(entry) > 1 and entry[0] == '.' and \
               '.' not in entry[1:] and \
               os.path.isdir(os.path.join(self._path, entry)):
                result.append(entry[1:])
        return result

    def get_folder(self, folder):
        """Return a Maildir instance for the named folder."""
        return Maildir(os.path.join(self._path, '.' + folder),
                       factory=self._factory,
                       create=False)

    def add_folder(self, folder):
        """Create a folder and return a Maildir instance representing it."""
        path = os.path.join(self._path, '.' + folder)
        result = Maildir(path, factory=self._factory)
        maildirfolder_path = os.path.join(path, 'maildirfolder')
        if not os.path.exists(maildirfolder_path):
            os.close(os.open(maildirfolder_path, os.O_CREAT | os.O_WRONLY))
        return result

    def _get_header(self):
        #RFC 3501 states: "A good UIDVALIDITY value to use in this case
        #is a 32-bit representation of the creation date/time of
        #the mailbox."
        uid_validity = int(round(os.path.getctime(self._path)))
        return '1 %d %d' % (uid_validity, 1)

    def _fwrite(self, path, msg, typ='append'):
        f = self._create_temp()
        mailbox._lock_file(f)
        if os.path.exists(path):
            src_f = open(path, 'r')
            mailbox._lock_file(src_f)
            for line in src_f:
                #Length of 3 indicates first line
                #(format, uid_validity, uid_next)
                #Length of 2 indicates a message
                #(uid, file_name)
                lsplit = line.split()
                if typ == 'header':
                    if len(lsplit) == 3:
                        f.write('%s%s' % (msg, os.linesep))
                    else:
                        f.write(line)
                elif typ == 'append':
                    if len(lsplit) == 3:
                        uid_validity = int(lsplit[1])
                        uid = int(lsplit[2])
                        f.write('%d %d %d%s' % (1, uid_validity, uid+1, os.linesep))
                    else:
                        f.write(line)
                elif typ == 'remove':
                    if lsplit[1].split(',')[0].split(self.colon)[0] == msg:
                        #Message filename found, pass to remove
                        #from destination file
                        pass
                    else:
                        f.write(line)
            else:
                #Finally append newly added message filename + uid
                if typ == 'append':
                    f.write('%d %s%s' % (uid, msg, os.linesep))
            mailbox._unlock_file(src_f)
        else:
            if typ == 'header':
                f.write('%s%s' % (msg, os.linesep))
            else:
                f.write('%s%s' % (self._get_header(), os.linesep))
                f.write('%s%s' % (msg, os.linesep))

        mailbox._unlock_file(f)
        mailbox._sync_close(f)
        try:
            self._emplace(f.name, path)
        except OSError, e:
            if e.errno == errno.EEXIST:
                os.remove(path)
                self._emplace(f.name, path)
            else:
                raise

    def _emplace(self, old_path, new_path):
        if hasattr(os, 'link'):
            os.link(old_path, new_path)
            os.remove(old_path)
        else:
            os.rename(old_path, new_path)

    def _refresh(self):
        """Update table of contents mapping."""
        self._toc = {}
        f = open(self._uidlist, 'r')
        try:
            for line in f:
                lsplit = line.split()
                #Length of 3 indicates first line
                #(format, uid_validity, uid_next)
                #Length of 2 indicates a message
                #(uid, file_name)
                if len(lsplit) == 2:
                    self._toc[int(lsplit[0])] = lsplit[1]
        finally:
            f.close()

    def _lookup(self, key):
        """Use TOC to return subpath for given key, or raise a KeyError."""
        try:
            base_fname = self._toc[key].split(',')[0].split(self.colon)[0]
            d_found = None
            for d in ('new', 'cur'):
                res = glob(os.path.join(self._path, d, '%s*' % base_fname))
                if len(res) > 0:
                    d_found = d
                    return os.path.join(d_found, res[0].split(os.sep)[-1])
        except KeyError:
            pass
        self._refresh()
        try:
            if d_found is not None:
                return os.path.join(d_found, res[0].split(os.sep)[-1])
            else:
                raise KeyError('No message with key: %s' % key)
        except KeyError:
            raise KeyError('No message with key: %s' % key)